/*******************************************************************************
 * Copyright (c) 2009 Kevin Bierhoff and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Kevin Bierhoff - initial API and implementation
 *******************************************************************************/
package com.googlecode.twelfdevtools.builder;

import java.io.IOException;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.resources.IResourceDeltaVisitor;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.swt.widgets.Display;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import com.googlecode.twelfdevtools.model.TwelfModel;
import com.googlecode.twelfdevtools.server.TwelfServer;
import com.googlecode.twelfdevtools.server.TwelfServerEvent;
import com.googlecode.twelfdevtools.server.TwelfServerListener;
import com.googlecode.twelfdevtools.views.ElfFileOutline;


public class TwelfBuilder extends IncrementalProjectBuilder {

	/**
	 * Resource delta visitor that fixes Twelf config files after renames, moves, and deletes.
	 * @author Kevin Bierhoff
	 */
	class ElfFileDeltaVisitor implements IResourceDeltaVisitor {
		
		/* (non-Javadoc)
		 * @see org.eclipse.core.resources.IResourceDeltaVisitor#visit(org.eclipse.core.resources.IResourceDelta)
		 */
		public boolean visit(IResourceDelta delta) throws CoreException {
			IResource resource = delta.getResource();
			switch (delta.getKind()) {
			case IResourceDelta.ADDED:
				// handle added resource
				if(isElfFile(resource) && delta.getMovedFromPath() != null) {
					IFile oldFile = getProject().getWorkspace().getRoot().getFile(delta.getMovedFromPath());
					if(! getProject().equals(oldFile.getProject())) {
						// moved from another project
						addToConfigFile(getProject(), (IFile) resource);
						// removed from old project in REMOVED case
					}
					// else handled in REMOVED case
				}
				break;
			case IResourceDelta.REMOVED:
				// handle removed resource
				if(isElfFile(resource)) {
					if(delta.getMovedToPath() != null) {
						IFile newFile = getProject().getWorkspace().getRoot().getFile(delta.getMovedToPath());
						if(getProject().equals(newFile.getProject())) {
							// moved / renamed inside the project
							renameInConfigFile(getProject(), (IFile) resource, newFile);
						}
						else {
							// moved to another project
							removeFromConfigFile(getProject(), (IFile) resource);
							// added to new project in ADDED case
						}
					}
					else {
						// not moved but deleted
						removeFromConfigFile(getProject(), (IFile) resource);
					}
				}
				break;
			case IResourceDelta.CHANGED:
				// handle changed resource
				// nothing: changes inside files are handled by subsequent Twelf invocation
				break;
			}
			// return true to continue visiting children.
			return true;
		}
		
		/**
		 * Tests whether the given resource is a file ending in ".elf".
		 * @param r
		 * @return <code>true</code> if the given resource is a file ending in ".elf",
		 * <code>false</code> otherwise.
		 */
		private boolean isElfFile(IResource r) {
			if(r instanceof IFile) {
				IFile f = (IFile) r;
				return "elf".equals(f.getFileExtension());
			}
			else
				return false;
		}
		
	}

	class SampleResourceVisitor implements IResourceVisitor {
		public boolean visit(IResource resource) {
			checkXML(resource);
			//return true to continue visiting children.
			return true;
		}
	}

	class XMLErrorHandler extends DefaultHandler {
		
		private IFile file;

		public XMLErrorHandler(IFile file) {
			this.file = file;
		}

		private void addMarker(SAXParseException e, int severity) {
			TwelfBuilder.this.addMarker(file, e.getMessage(), e
					.getLineNumber(), severity);
		}

		public void error(SAXParseException exception) throws SAXException {
			addMarker(exception, IMarker.SEVERITY_ERROR);
		}

		public void fatalError(SAXParseException exception) throws SAXException {
			addMarker(exception, IMarker.SEVERITY_ERROR);
		}

		public void warning(SAXParseException exception) throws SAXException {
			addMarker(exception, IMarker.SEVERITY_WARNING);
		}
	}

	public static final String BUILDER_ID = "com.googlecode.twelfdevtools.twelfBuilder";

	public static final String TWELF_MARKER_TYPE = "com.googlecode.twelfdevtools.twelfMarker";
	
	public static final String PROBLEM_MARKER_TYPE = "com.googlecode.twelfdevtools.twelfProblem";

	public static final String TYPING_MARKER_TYPE = "com.googlecode.twelfdevtools.twelfTyping";

	public static final String ID_ATTRIBUTE = "com.googlecode.twelfdevtools.twelfId";

	public static final String CHAR_DECL_END = "com.googlecode.twelfdevtools.twelfDeclEndChar";

	private SAXParserFactory parserFactory;
	
	private TwelfServer twelf;
	
	public TwelfBuilder() {
		super();
	}

	private void addMarker(IFile file, String message, int lineNumber,
			int severity) {
		try {
			IMarker marker = file.createMarker(PROBLEM_MARKER_TYPE);
			marker.setAttribute(IMarker.MESSAGE, message);
			marker.setAttribute(IMarker.SEVERITY, severity);
			if (lineNumber == -1) {
				lineNumber = 1;
			}
			marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
		} catch (CoreException e) {
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.core.internal.events.InternalBuilder#build(int,
	 *      java.util.Map, org.eclipse.core.runtime.IProgressMonitor)
	 */
	protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
			throws CoreException {
		try {
			if (kind == FULL_BUILD) {
				fullBuild(monitor);
			} else {
				IResourceDelta delta = getDelta(getProject());
				if (delta == null) {
					fullBuild(monitor);
				} else {
					incrementalBuild(delta, monitor);
				}
			}
		} 
		catch (IOException e) {
			System.out.println("Error running Twelf builder");
			e.printStackTrace();
		}
		return null;
	}

	void checkXML(IResource resource) {
		if (resource instanceof IFile && resource.getName().endsWith(".xml")) {
			IFile file = (IFile) resource;
			deleteMarkers(file);
			XMLErrorHandler reporter = new XMLErrorHandler(file);
			try {
				getParser().parse(file.getContents(), reporter);
			} catch (Exception e1) {
			}
		}
	}

	private void deleteMarkers(IFile file) {
		try {
			file.deleteMarkers(TWELF_MARKER_TYPE, false, IResource.DEPTH_ZERO);
		} catch (CoreException ce) {
		}
	}

	protected void fullBuild(final IProgressMonitor monitor)
			throws CoreException, IOException {
		getProject().deleteMarkers(TWELF_MARKER_TYPE, true, IResource.DEPTH_INFINITE);
		getTwelf().make();
	}
	
	/**
	 * Returns the Twelf process used by this builder.  
	 * The process is instantiated if not already present.
	 * @return the Twelf process used by this builder; 
	 * <code>null</code> if this builder is not associated with a project.
	 * @throws IOException If Twelf could not be instantiated.
	 */
	private TwelfServer getTwelf() throws IOException {
		if(twelf != null)
			return twelf;
		if(getProject() != null) {
			twelf = new TwelfServer(getProject());
			// collects types and annotates the respective source files
			twelf.addTwelfServerListener(new TwelfTypingListener(getProject()));
			// test listener that prints out events
			twelf.addTwelfServerListener(new TwelfServerListener() {

				public void notifyTwelfServerEvent(TwelfServerEvent e) {
					switch(e.getEventType()) {
					case CONSTANT:
					case PRAGMA:
						System.out.println(e.getRawInfo());
					}
				}
				
			});
		}
		return twelf;
	}

	private SAXParser getParser() throws ParserConfigurationException,
			SAXException {
		if (parserFactory == null) {
			parserFactory = SAXParserFactory.newInstance();
		}
		return parserFactory.newSAXParser();
	}

	protected void incrementalBuild(IResourceDelta delta,
			IProgressMonitor monitor) throws CoreException, IOException {
		// update config file due to deleted and renamed files
		delta.accept(new ElfFileDeltaVisitor());

		// Twelf server can only fully build
		fullBuild(monitor);
	}
	
	protected void finalize() {
		if(twelf != null)
			twelf.quit();
	}

	public static void addToConfigFile(IProject twelfProject, IFile file) throws CoreException {
		IPath path = TwelfModel.getConfigFile(twelfProject).getFullPath();
		IPath relativeFilePath = file.getProjectRelativePath();
		String fileName = relativeFilePath.toPortableString();
		
		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		manager.connect(path, LocationKind.IFILE, null);

		try {
			ITextFileBuffer buffer = manager.getTextFileBuffer(path, LocationKind.IFILE);
			final IDocument doc = buffer.getDocument();
			String content = doc.get();
			if(content.contains(fileName))
				// already in the config file
				return;
			
			try {
				boolean doCommit = buffer.isDirty() == false;
				int lastLineNumber = doc.getLineOfOffset(doc.getLength());
				final String append;
				if(doc.getLineLength(lastLineNumber) == 0) {
					// last line is empty -> put filename into it
					append = fileName + "\n";
				}
				else {
					// put filename into new line
					append = "\n" + fileName + "\n";
				}
				Display.getDefault().syncExec(new Runnable() {
					public void run() {
						try {
							doc.replace(doc.getLength(), 0, append);
						} catch (BadLocationException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				});
				if (doCommit)
					buffer.commit(null, false);
			} 
			catch (BadLocationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} 
		finally {
			try {		
				manager.disconnect(path, LocationKind.IFILE, null);
			} 
			catch (CoreException ex) {
				ex.printStackTrace();
			}
		}
	}

	public static void removeFromConfigFile(IProject twelfProject, IFile file) throws CoreException {
		IPath path = TwelfModel.getConfigFile(twelfProject).getFullPath();
		IPath relativeFilePath = file.getProjectRelativePath();
		String fileName = relativeFilePath.toPortableString();
		
		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		manager.connect(path, LocationKind.IFILE, null);

		try {
			ITextFileBuffer buffer = manager.getTextFileBuffer(path, LocationKind.IFILE);
			final IDocument doc = buffer.getDocument();
			String content = doc.get();
			int start = content.indexOf(fileName);
			if(start < 0)
				// not in config file
				return;
						
			try {
				boolean doCommit = buffer.isDirty() == false;
				int lineNumber = doc.getLineOfOffset(start);
				IRegion lineInfo = doc.getLineInformation(lineNumber);
				final int lineOffset;
				final int lineLength;
				if(lineInfo.getLength() == fileName.length()) {
					// cut out entire line including line delimiter
					lineOffset = lineInfo.getOffset();
					lineLength = lineInfo.getLength() + doc.getLineDelimiter(lineNumber).length();
				}
				else {
					// line contains other stuff besides file name
					lineOffset = start;
					lineLength = fileName.length();
				}
				
				// update document in UI thread necessary if document is open in editor
				Display.getDefault().syncExec(new Runnable() {
					public void run() {
						try {
							doc.replace(lineOffset, lineLength, "");
						} catch (BadLocationException e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
					}
				});
				if (doCommit)
					buffer.commit(null, false);
			} 
			catch (BadLocationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		} 
		finally {
			try {		
				manager.disconnect(path, LocationKind.IFILE, null);
			} 
			catch (CoreException ex) {
				ex.printStackTrace();
			}
		}
	}

	public static void renameInConfigFile(IProject twelfProject, IFile oldFile, IFile newFile) throws CoreException {
		IPath path = TwelfModel.getConfigFile(twelfProject).getFullPath();
		final String oldFileName = oldFile.getProjectRelativePath().toPortableString();
		final String newFileName = newFile.getProjectRelativePath().toPortableString();
		
		ITextFileBufferManager manager= FileBuffers.getTextFileBufferManager();
		manager.connect(path, LocationKind.IFILE, null);

		try {
			final ITextFileBuffer buffer = manager.getTextFileBuffer(path, LocationKind.IFILE);
			final IDocument doc = buffer.getDocument();
			String content = doc.get();
			final int start = content.indexOf(oldFileName);
			if(start < 0)
				// not in config file
				return;
			
			boolean doCommit = buffer.isDirty() == false;
			// update document in UI thread necessary if document is open in editor
			Display.getDefault().syncExec(new Runnable() {
				public void run() {
					try {
						doc.replace(start, oldFileName.length(), newFileName);
					} catch (BadLocationException e) {
						// TODO Auto-generated catch block
						e.printStackTrace();
					}
				}
			});
			if (doCommit)
				// save, if the document wasn't changed already
				buffer.commit(null, false);
		} 
		finally {
			try {		
				manager.disconnect(path, LocationKind.IFILE, null);
			} 
			catch (CoreException ex) {
				ex.printStackTrace();
			}
		}
	}
}
